﻿using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace cellAnalysis
{
    public class DeepCopyer
        {
            public DeepCopyer()
            {
            }

            public object copy(object original)
            {
                if (original == null) return null;
                object result;
                if (!clones.TryGetValue(original, out result))
                {
                    result = methods.GetCloneMethod(original.GetType())(this, original);
                }
                return result;
            }

            private void register(object original, object clone)
            {
                clones[original] = clone;
            }

            public static object Copy(object obj)
            {
                DeepCopyer dc = new DeepCopyer();
                return dc.copy(obj);
            }

            public static T Copy<T>(T obj)
            {
                return (T)Copy((object)obj);
            }

            public Dictionary<object, object> clones = new Dictionary<object, object>(64, ReferenceComparer.Default);

            class ReferenceComparer : IEqualityComparer<object>
            {

                protected ReferenceComparer()
                {
                }

                public static readonly ReferenceComparer Default = new ReferenceComparer();

                bool IEqualityComparer<object>.Equals(object x, object y)
                {
                    return object.ReferenceEquals(x, y);
                }

                int IEqualityComparer<object>.GetHashCode(object obj)
                {
                    return RuntimeHelpers.GetHashCode(obj);
                }
            }

            private static DynamicTypes methods = new DynamicTypes();

            private class DynamicTypes
            {
                public DynamicTypes()
                {
                    objMethod[typeof(string)] = new cloneDelegate(strCopy);
                }

                //Weil string nicht alle seine Daten in seinen Feldern speichert, ist eine Sonderbehandlung nötig:
                public static object strCopy(DeepCopyer d, object original)
                {
                    object result = ((string)original).Clone();
                    d.register(original, result);
                    return result;
                }

                private static void IterateArray(DeepCopyer dc, Array array)
                {
                    int[] indices = new int[array.Rank];
                    int i;
                    for (i = 0; i < indices.Length; i++)
                    {
                        int lowerBound = array.GetLowerBound(i);
                        indices[i] = lowerBound;
                        if (lowerBound > array.GetUpperBound(i))
                        {
                            return;//Array ist leer
                        }
                    }
                    i = 0;
                    while (i >= 0)
                    {
                        array.SetValue(dc.copy(array.GetValue(indices)), indices);
                        i = indices.Length - 1;
                        while (i >= 0)
                        {
                            int ai = indices[i];
                            ai++;
                            if (ai > array.GetUpperBound(i))
                            {
                                indices[i] = array.GetLowerBound(i);
                                i--;
                                continue;
                            }
                            else
                            {
                                indices[i] = ai;
                                break;
                            }
                        }
                    }
                }

                Dictionary<Type, cloneDelegate> objMethod = new Dictionary<Type, cloneDelegate>(32);
                MethodInfo memberwiseCloneInfo = typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod);
                MethodInfo copyInfo = typeof(DeepCopyer).GetMethod("copy", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod);
                MethodInfo registerInfo = typeof(DeepCopyer).GetMethod("register", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod);
                MethodInfo iterateArrayInfo = typeof(DynamicTypes).GetMethod("IterateArray", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod);
                public delegate object cloneDelegate(DeepCopyer copyer, object original);

                public cloneDelegate GetCloneMethod(Type t)
                {
                    cloneDelegate del;
                    //Ist Kopiermethode vorhanden? :
                    if (!objMethod.TryGetValue(t, out del))
                    {
                        //wenn nicht:
                        //erzeuge Methode:
                        DynamicMethod Method = new DynamicMethod("DeepCopyer<>Copy" + t.Name.Replace(".", "<>"),
                            typeof(object), new Type[2] { typeof(DeepCopyer), typeof(object) }, true);

                        ILGenerator gen = Method.GetILGenerator();

                        //object var0;
                        gen.DeclareLocal(typeof(object));
                        //<t> var1;
                        gen.DeclareLocal(t);
                        //object var2;
                        gen.DeclareLocal(typeof(object));

                        //arg1.MemberwiseClone();
                        gen.Emit(OpCodes.Ldarg_1);
                        gen.EmitCall(OpCodes.Call, memberwiseCloneInfo, null);
                        //var0=<letzterRückgabewert>;
                        gen.Emit(OpCodes.Stloc_0);
                        //arg0.register(arg1,var0);
                        gen.Emit(OpCodes.Ldarg_0);
                        gen.Emit(OpCodes.Ldarg_1);
                        gen.Emit(OpCodes.Ldloc_0);
                        gen.EmitCall(OpCodes.Call, registerInfo, null);
                        if (!t.IsArray)
                        {
                            //prüfe, ob Felder vorhanden, die Referenztypen sind:
                            FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                            bool ContainsReferenceTypes = false;
                            foreach (FieldInfo f in fields)
                            {
                                if (!f.FieldType.IsValueType)
                                {
                                    ContainsReferenceTypes = true;
                                    break;
                                }
                            }
                            if (ContainsReferenceTypes)
                            {
                                //wenn ja:
                                //ersetze referenzierte Objekte durch Kopieen:
                                //var1=(<t>)var0;
                                gen.Emit(OpCodes.Ldloc_0);
                                gen.Emit(OpCodes.Castclass, t);
                                gen.Emit(OpCodes.Stloc_1);
                                foreach (FieldInfo f in fields)
                                {
                                    if (!f.FieldType.IsValueType)
                                    {
                                        //var2=(object)var1.<f>;
                                        gen.Emit(OpCodes.Ldloc_1);
                                        gen.Emit(OpCodes.Ldfld, f);
                                        gen.Emit(OpCodes.Castclass, typeof(object));
                                        gen.Emit(OpCodes.Stloc_2);

                                        //var2=var0.copy(var2);
                                        gen.Emit(OpCodes.Ldarg_0);
                                        gen.Emit(OpCodes.Ldloc_2);
                                        gen.EmitCall(OpCodes.Call, copyInfo, null);
                                        gen.Emit(OpCodes.Stloc_2);

                                        //var1.<f>=(<f.Type>)var2;
                                        gen.Emit(OpCodes.Ldloc_1);
                                        gen.Emit(OpCodes.Ldloc_2);
                                        gen.Emit(OpCodes.Castclass, f.FieldType);
                                        gen.Emit(OpCodes.Stfld, f);
                                    }
                                }
                            }
                        }
                        else
                        {
                            Type elementType = t.GetElementType();
                            if (elementType.IsPrimitive)
                                goto verarbeitet;
                            if (elementType.IsValueType)
                            {
                                FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                                bool ContainsReferenceTypes = false;
                                foreach (FieldInfo f in fields)
                                {
                                    if (!f.FieldType.IsValueType)
                                    {
                                        ContainsReferenceTypes = true;
                                        break;
                                    }
                                }
                                if (!ContainsReferenceTypes)
                                    goto verarbeitet;
                            }
                            {
                                //im Array jedes Element auswechseln:
                                gen.Emit(OpCodes.Ldarg_0);
                                gen.Emit(OpCodes.Ldloc_0);
                                gen.Emit(OpCodes.Castclass, typeof(Array));
                                gen.EmitCall(OpCodes.Call, iterateArrayInfo, null);
                            }
                        verarbeitet: { }
                        }
                        //return var0;
                        gen.Emit(OpCodes.Ldloc_0);
                        gen.Emit(OpCodes.Ret);
                        del = (cloneDelegate)Method.CreateDelegate(typeof(cloneDelegate));
                        objMethod[t] = del;
                    }
                    return del;
                }
            }
        }
}
